/**
 * LSDatabase.java
 *
 * @author Hagen Boehm
 */


package SSF.OS.OSPFv2;


import java.util.*;
import SSF.OS.ProtocolException;
import SSF.OS.ProtocolMessage;
import SSF.Net.Util.IP_s;

// ====================== class SSF.OS.OSPFv2.LSDatabase ===================== //
/**
 * An OSPF Link State database.
 */ 
public class LSDatabase {

  // ........................ member data .......................... //

  /** A reference to the OSPF session this database belongs to. */
  private OSPF myOSPF;
  /** The area data structure instance with which this database is associated. */
  private AreaData myArea;
  
  /** HashMaps for keeping all LSA's belonging to one area. For each internal 
   *  LSA type an associated HashMap should be implemented. Actually we just 
   *  deal with two types router- and summery-LSA. The key is a String containing
   *  LS type LS id and advertising router seperated by a space. The value is
   *  an LSASlot holding the LSA and its install timestamp.
   */
  public HashMap routerLSAs = new HashMap();
  public HashMap summaryLSAs = new HashMap();

  /** A boolean variable to indicate if this OSPF Database has been modified
   *  since last operated on by Dijkstra. */
  private boolean isModified = false;

  /** A boolean variable indicating that the OSPF forwarding-table must be recalculated. */
  private boolean buildFWT = false;

  // ------------------------ constructor LSDatabase ----------------------- //
  public LSDatabase(OSPF ospf, AreaData area) {
    myOSPF = ospf;
    myArea = area;
  }
  
  // ----------------------- LSDatabase.installOwnLSA ------------------------- //
  /** installs a new self-originated LSA if possible. Return true if installing
   *   was successful else false.
   */
  public boolean installOwnLSA(LSA newLSA) {
    String key = newLSA.getKey();
    OwnLSASlot slot;
    HashMap searchLSAs = getLSAHashMap(newLSA.lsType);
        
    if (searchLSAs.containsKey(key)) { 
      // got a new instance of a known LSA
      slot = (OwnLSASlot)searchLSAs.get(key);
      LSA oldLSA = slot.lsa;
      try {
	slot.install(newLSA);
      } catch (ProtocolException instExc) {
	if (myOSPF.debugOptions[9] == true) myOSPF.printMessage(instExc.getMessage());
        return false;
      }
      myArea.clearLSAFromRetrLists(oldLSA.getKey());
      // determine whether fwd_table must be rebuild.
      if (newLSA.lsType == LSA.ROUTER_LSA) buildFWT = shouldRecalcRoutTable(newLSA, oldLSA);
      isModified = true;
    } else {
      // got the first instance of an LSA
      slot = new OwnLSASlot(newLSA, myOSPF, myArea);
      searchLSAs.put(key, slot);
      isModified = true;
    }
    if (buildFWT) {
      myOSPF.rtcControlTimer.buildRoutingTable();
      buildFWT = false;
    }
    return true;
  }

  // -------------------------- LSDatabase.installRecLSA ---------------------------- //
  /** installs an LSA received during the flooding process or in database exchange
   *  process if possible. Return true if installing was successful else false.
   */
  public boolean installRecLSA(LSA newLSA) {
    String key = newLSA.getKey();
    RecLSASlot slot;
    HashMap searchLSAs = getLSAHashMap(newLSA.lsType);

    if (searchLSAs.containsKey(key)) {
      // got a new instance of a known LSA
      slot = (RecLSASlot)searchLSAs.get(key);
      LSA oldLSA = slot.lsa;
      try {
	slot.install(newLSA);
      } catch (ProtocolException instExc) {
	if (myOSPF.debugOptions[9] == true || myOSPF.debugOptions[4] == true)
	  myOSPF.printMessage(instExc.getMessage());
        return false;
      }
      myArea.clearLSAFromRetrLists(oldLSA.getKey());
      // determine whether fwd_table must be rebuild.
      buildFWT = shouldRecalcRoutTable(newLSA, oldLSA);
      isModified = true;
    } else {
      // got the first instance of an LSA
      slot = new RecLSASlot(newLSA, myOSPF);
      searchLSAs.put(key, slot);
      buildFWT = true;
      isModified = true;
    }
    if (buildFWT) {
      switch (newLSA.lsType) {
      case LSA.ROUTER_LSA:
        myOSPF.rtcControlTimer.buildRoutingTable();
      break;
      case LSA.SUMMARY_NETWORK_LSA:
      case LSA.SUMMARY_ASBR_LSA:
        if (newLSA.payload() instanceof Summary_LSA) {
	  myOSPF.rtcControlTimer.processInterAreaRoute(newLSA, myArea.areaID);
        }
      break;
      }
      buildFWT = false;
    }
    return true;
  }

  // -------------------- LSDatabase.shouldRecalcRoutTable ------------------------ //
  /** Return true if the new LSA to install differs from the old LSA already stored 
   *  here, otherwise false. So this method returns whether the Routing Table should
   *  be recalculated, or not.
   */
  public boolean shouldRecalcRoutTable(LSA newLSA, LSA oldLSA) {
    if (!Arrays.equals(newLSA.options, oldLSA.options))
      return true;
    if (((newLSA.lsAge == myOSPF.MAX_AGE) && (oldLSA.lsAge != myOSPF.MAX_AGE)) || 
        ((oldLSA.lsAge == myOSPF.MAX_AGE) && (newLSA.lsAge != myOSPF.MAX_AGE)))
      return true;
    if (newLSA.bytecount() != oldLSA.bytecount())
      return true;
    ProtocolMessage newLoad = newLSA.payload();
    ProtocolMessage oldLoad = oldLSA.payload();
    if (newLoad.equals(oldLoad)) return false; // Contents are equal.
    else return true; // Contents are different.
  }

  // ---------------------- LSDatabase.getAllLSAHeaders --------------------------- //
  /** 
   * return all LSA Headers (Router and Summary) in a Vector.
   */
  public Vector getAllLSAHeaders() {
    Vector summList = new Vector();
    for (Iterator slotVals = routerLSAs.values().iterator(); slotVals.hasNext(); ) {
      // add Router-LSA Header
      summList.add(((LSASlot)slotVals.next()).getLSAHeader());
    }
    for (Iterator slotVals = summaryLSAs.values().iterator(); slotVals.hasNext(); ) {
      // add Summary-LSA Header
      summList.add(((LSASlot)slotVals.next()).getLSAHeader());
    }
    return summList;
  }

  // -------------------------- LSDatabase.getLSAHashMap ------------------------------ //
  /** Return the HashMap in which LSAs of the type given in the parameter type are stored. 
   *  This Method is used by "hasRightInstanceOf", "hasInstanceOf" and "isSelforiginated".
   */
  public HashMap getLSAHashMap(byte type) {
    switch(type) {
    case LSA.ROUTER_LSA:
      return routerLSAs;
    case LSA.SUMMARY_NETWORK_LSA:
    case LSA.SUMMARY_ASBR_LSA:
      return summaryLSAs;
    default: return null; // This case can not happen (it is intercepted in method "processLSA",
	                  // "processLSUpdate" and "processReceivedLSA" of class Neighbor).
    }
  }

  // ----------------------- LSDatabase.hasRightInstanceOf ---------------------------- //
  /** Check whether an instance of the given LSA exists and if so return true
   *  if this instance is newer than the given one else false.
   */
  public boolean hasRightInstanceOf(LSA testLSA) {
    String key = testLSA.getKey();
    HashMap searchLSAs = getLSAHashMap(testLSA.lsType);
    if (searchLSAs == null) return true; // in this case we don't accept the LSA!
    if (searchLSAs.containsKey(key)) {
      String compare = isNewerThan(testLSA, ((LSASlot)searchLSAs.get(key)).lsa);
      if ((compare.equals("false")) || (compare.equals("identical"))) return true;
    }
    return false;
  }

  // -------------------------- LSDatabase.hasInstanceOf ------------------------------- //
  /** Check whether an instance of the given LSA exists and if so return true else false. */
  public boolean hasInstanceOf(LSA testLSA) {
    String key = testLSA.getKey();
    HashMap searchLSAs = getLSAHashMap(testLSA.lsType);
    if (searchLSAs == null) return true; // in this case we don't accept the LSA!
    if (searchLSAs.containsKey(key)) return true;
    else return false;
  }

  // ------------------------------ LSDatabase.getLSA ---------------------------------- //
  /** Determine whether a particular LSA is stored in this database, and if so return a 
   *  complete copy of this LSA, otherwise return null.
   *  @param key String representing the unique identifier of an LSA. (see LSA.getKey).
   *  @return LSA containing the desired LSA or null if the LSA could not be found.
   */
  public LSA getLSA(String key) {
    if (routerLSAs.containsKey(key)) 
      return ((LSASlot)routerLSAs.get(key)).getLSA();
    if (summaryLSAs.containsKey(key)) 
      return ((LSASlot)summaryLSAs.get(key)).getLSA();
    return null;
  }

  // --------------------------- LSDatabase.getNextSeqNum ------------------------------- //
  /** Return the sequence number of the next instance of the LSA specified by its key and its type.
   *  This method is used in case of selforiginated LSAs only!
   *  @param lsaKey String representing the key value of the LSA.
   *  @param lsaType byte value indicating the type of the LSA.
   *  @return int value representing the next sequence number of the LSA.
   */
  public int getNextSeqNum(String lsaKey, byte lsaType) {
    int newSeqNum;
    HashMap searchLSAs = getLSAHashMap(lsaType);
    if (searchLSAs.containsKey(lsaKey)) {
      newSeqNum = ((OwnLSASlot)searchLSAs.get(lsaKey)).nextSeqNum;
    } else {
      newSeqNum = myOSPF.INIT_SEQUENCE_NUM;
    }
    return newSeqNum;
  }

  // ---------------------------- LSDatabase.setNextSeqNum ----------------------------------- //
  /** Set value of nextSeqNum of the LSA specified by lsaKey and lsaType to newSeqNum. Only
   *  used in case of selforiginated LSAs!
   *  @param lsaKey String representing the key value of the LSA.
   *  @param lsaType byte value indicating the type of the LSA.
   *  @param newSeqNum The new sequence number of the next LSA instance to be installed.
   */
  public void setNextSeqNum(String lsaKey, byte lsaType, int newSeqNum) {
    HashMap searchLSAs = getLSAHashMap(lsaType);
    OwnLSASlot slot = (OwnLSASlot)searchLSAs.get(lsaKey);
    if (slot.nextSeqNum == myOSPF.MAX_SEQUENCE_NUM + 1) return;
    if (slot.nextSeqNum < newSeqNum || newSeqNum == myOSPF.MAX_SEQUENCE_NUM + 1) slot.nextSeqNum = newSeqNum;
  }

  // ---------------------------- LSDatabase.isNewerThan ------------------------------- //
  /** Take two instances of an LSA and determine which of them is newer. Return
   *  true      if lsa1 is newer than lsa2, 
   *  false     if lsa2 is newer than lsa1,
   *  identical if this method couldn't find any difference.
   *
   *  @param lsa1 first LSA to compare.
   *  @param lsa2 second LSA to compare.
   *
   *  @return String holding "true", "false" or "identical".
   */
  public String isNewerThan(LSA lsa1, LSA lsa2) {
    int maxAge = myOSPF.MAX_AGE;
    int maxAgeDiff = myOSPF.MAX_AGE_DIFF;
    // LSA having the newer LS sequence number is more recent.
    if (lsa1.lsSeqnum > lsa2.lsSeqnum) {
      return "true";
    } else {
      if (lsa1.lsSeqnum < lsa2.lsSeqnum) {
        return "false";
      }
    }
    // if only one instance has MAX_AGE, this one is more recent.
    if ((lsa1.lsAge == maxAge) && (lsa2.lsAge != maxAge)) {
      return "true";
    } else {
      if ((lsa1.lsAge != maxAge) && (lsa2.lsAge == maxAge)) {
        return "false";
      }
    }
    // LSA having the younger age is more recent if both differ by more than MAX_AGE_DIFF.
    if (java.lang.Math.abs(lsa1.lsAge - lsa2.lsAge) > maxAgeDiff) {
      if (lsa1.lsAge < lsa2.lsAge) return "true";
      else return "false";
    }
    // the two instances are considered to be identical.
    return "identical";
  }

  // --------------------------- LSDatabase.prematureAgeLSA ---------------------------- //
  /** Set a particular LSA's age to MAX_AGE and flood it out to flush it from the routing
   *  domain. As soon as it is no longer contained on any neighbor link state retransmission
   *  list and non of the router's neighbors are in states EXCHANGE or LOADING, it will be
   *  removed. This method supports Summary-LSAs only. For Router-LSAs premature aging is
   *  integrated in the creation process.
   *  @param type byte value indicating the type of the LSA to be premature aged.
   *  @param lsID int value representing an ip address of a network (LSA type 3) or the
   *              ip address which serves as id of the destination router (LSA type 4).
   */
  public void prematureAgeLSA(byte type, int lsID) {
    String lsaKey = type + " " + IP_s.IPtoString(lsID) + " " + IP_s.IPtoString(myOSPF.rtrid);
    LSASlot slot;
    switch (type) {
    case LSA.SUMMARY_NETWORK_LSA:
    case LSA.SUMMARY_ASBR_LSA:
      slot = (LSASlot)summaryLSAs.get(lsaKey);
      if (slot == null) return; // there is no LSA to premature age.
      if (slot instanceof OwnLSASlot) {
	// do only premature age LSAs that are selforiginated.
	if (slot.lsa.lsAge < myOSPF.MAX_AGE) {
	  slot.lsa.lsAge = myOSPF.MAX_AGE;
          isModified = true;
          myArea.floodLSA(slot.lsa, myOSPF.rtrid);
        }
      } else {
	myOSPF.printGeneralWarning("Selforiginated LSA (" + lsaKey + 
	                           ") is NOT installed in an OwnLSASlot in the LS database of area: "
                                   + myArea.areaID);
      }
    break;
    default:
    break;
    }
  }

  // --------------------------- LSDatabase.ageAndCheckLSAs ----------------------------- //
  /** Age all LSAs in this Area and check self-originated LSAs for reorigination. */
  public void ageAndCheckLSAs() {
    ageAndCheckLSAsOfType(LSA.ROUTER_LSA);
    ageAndCheckLSAsOfType(LSA.SUMMARY_NETWORK_LSA);
    myArea.databaseDebugInfo();
  }

  // ------------------------ LSDatabase.ageAndCheckLSAsOfType -------------------------- //
  /** Called by "ageAndCheckLSAs()" to age a particular kind of LSAs and check self-originated
   *  LSAs whether they must be reoriginated or not.
   *  @param lsType byte value, indicating the type of the LSAs to be aged and checked.
   */
  public void ageAndCheckLSAsOfType(byte lsType) {
    LSASlot nextSlot;
    HashMap lsaMap = getLSAHashMap(lsType);
    boolean gotMaxAge = false;

    Object[] lsaArray = lsaMap.values().toArray();
    for (int i=0; i < lsaArray.length; i++) {
      nextSlot = (LSASlot)lsaArray[i];
      if (nextSlot.lsaHasMaxAge()) {
        // LSA in current slot had reached MAX_AGE some time before.
	// Flooding process to remove LSA is running. Check whether it has terminated. 
        if (myArea.canBeRemovedFromDatabase(nextSlot.lsa)) {
	  // All Neighbors have acknowleged and no one is in EXCHANGE or LOADING.
          if (nextSlot instanceof OwnLSASlot) {
            // LSA is selforiginated. 
            if (!((OwnLSASlot)nextSlot).lsaIsProtected()) {
	      int destIP = nextSlot.lsa.lsID;
              int cost;
              switch (lsType) {
              case LSA.ROUTER_LSA:
                lsaMap.remove(nextSlot.lsa.getKey());
                buildFWT = true;
	        myArea.createRouterLSA();
              break;
              case LSA.SUMMARY_NETWORK_LSA:
                int destMask = ((Summary_LSA)nextSlot.lsa.payload()).netMask;
                cost = myOSPF.routingTable.getNetCost(destIP + "/" + destMask, myArea.areaID);
                lsaMap.remove(nextSlot.lsa.getKey());
                if (cost != -1) {
	          LSA sumLSA = myArea.createSummary3LSA(destIP, destMask, cost);
                  myArea.installSelfOrigLSA(sumLSA);
                }
              break;
              case LSA.SUMMARY_ASBR_LSA:
                cost = myOSPF.routingTable.getRoutCost(new Integer(destIP), myArea.areaID);
                lsaMap.remove(nextSlot.lsa.getKey());
                if (cost != -1) {
	          LSA sumLSA = myArea.createSummary4LSA(destIP, cost);
                  myArea.installSelfOrigLSA(sumLSA);
                }
	      break;
              }
            }
          } else {
            // LSA was received from a Neighbor. Inform about modification.
            lsaMap.remove(nextSlot.lsa.getKey());
            isModified = true;
          }
        }
      } else {
        // age LSA.
	gotMaxAge = nextSlot.ageLSA();
        if (gotMaxAge && (nextSlot instanceof RecLSASlot)) {
          // LSA just reached MAX_AGE and LSA was received from a Neighbor.
          myArea.floodLSA(nextSlot.lsa, myOSPF.rtrid); // Reflood a MAX_AGE LSA.
          isModified = true; // inform about modification.
          switch (lsType) {
          case LSA.ROUTER_LSA:
            myOSPF.rtcControlTimer.buildRoutingTable(); // Rebuild the routing table.
	  break;
          case LSA.SUMMARY_NETWORK_LSA:
          case LSA.SUMMARY_ASBR_LSA:
	    // remove an inter area route from the OSPFv2 routing table
	    myOSPF.rtcControlTimer.processInterAreaRoute(nextSlot.lsa, myArea.areaID);
	  break;
          }
        } 
      }
    }
  }
    
  // ---------------------------- LSDatabase.printDatabase ------------------------- //
  /** 
   * Print lists of all Router- and Summary-LSAs stored in this database.
   */
  public void printDatabase() {
    if (isModified) {
      if (myOSPF.debugOptions[9] == true || myOSPF.debugOptions[0] == true) {
	myOSPF.printMessage("|LS Database of Area " + myArea.areaID + " changed! Content is now:");
        LSASlot slot;
        String db = "LS Type     Link State ID    Adv. Router    LS Sequence No.     LS Age\n"
                   +"----------------------------------------------------------------------\n";
        System.out.print(db);
        for (Iterator entryKeys = routerLSAs.values().iterator(); entryKeys.hasNext(); ) {
          slot = (LSASlot)entryKeys.next();
          System.out.println(slot.lsa.toString());
        }
        for (Iterator entryKeys = summaryLSAs.values().iterator(); entryKeys.hasNext(); ) {
          slot = (LSASlot)entryKeys.next();
          System.out.println(slot.lsa.toString());
        }
        System.out.print("\n\n");
        isModified = false;
      }
    }
  }

  // ----------------------- LSDatabase.getRouterAndNetworkLSAs -------------------------- //
  /** Return all router and network LSAs in a Vector. This method is called when 
   *  calculating a new routing table. SSF-Net does not support broadcast so there are NO
   *  network LSAs stored here at this time.
   */
  public Vector getRouterAndNetworkLSAs() {
    Vector lsas = new Vector();
    for (Iterator routs = routerLSAs.values().iterator(); routs.hasNext(); )
      lsas.add(((LSASlot)routs.next()).lsa);
    // for (Iterator nets = networkLSAs.values().iterator(); nets.hasNext(); )
    //   lsas.add(((LSASlot)nets.next()).lsa);
    return lsas;
  }

  // ------------------------------- LSDatabase.clear ------------------------------------ //
  /** clear the LSDatabase. Remove all LSAs from the HashMaps. */
  public void clear() {
    routerLSAs.clear();
    summaryLSAs.clear();
  }

} // end class LSDatabase
